/*
 * Copyright (C) 2018 Jolla Ltd.
 * Contact: Chris Adams <chris.adams@jollamobile.com>
 * All rights reserved.
 * BSD 3-Clause License, see LICENSE.
 */

#include "Crypto/generatestoredkeyrequest.h"
#include "Crypto/generatestoredkeyrequest_p.h"

#include "Crypto/cryptomanager.h"
#include "Crypto/cryptomanager_p.h"
#include "Crypto/serialization_p.h"

#include <QtDBus/QDBusPendingReply>
#include <QtDBus/QDBusPendingCallWatcher>

using namespace Sailfish::Crypto;

GenerateStoredKeyRequestPrivate::GenerateStoredKeyRequestPrivate()
    : m_status(Request::Inactive)
{
}

/*!
  \qmltype GenerateStoredKeyRequest
  \brief Allows a client request that the system crypto service generate and secure store a key based on a template.
  \inqmlmodule Sailfish.Crypto
  \inherits Request
  \instantiates Sailfish::Crypto::GenerateStoredKeyRequest
*/

/*!
  \class GenerateKeyRequest
  \brief Allows a client request that the system crypto service generate and secure store a key based on a template.
  \inmodule SailfishCrypto
  \inheaderfile Crypto/generatestoredkeyrequest.h

  The generated key will be stored securely by the crypto daemon via the storage
  plugin identified by the storage plugin specified in the key template's
  identifier, and the returned key reference will not contain any private or secret
  key data.

  Available storage providers can be enumerated from the Sailfish Secrets API.

  If the cryptoPluginName() is the same as the Key::Identifier::storagePluginName(),
  then the key will be stored in storage managed by the crypto provider plugin, if
  that plugin supports storing keys.
  In that case, the crypto plugin must also be a Sailfish::Secrets::EncryptedStoragePlugin.
  Such crypto storage plugins can enforce key component readability constraints,
  and allow cryptographic operations to occur in the most secure manner possible.

  When generating a key for a symmetric cipher algorithm, the client
  application can request that a specific key derivation function
  be used to derive the secret key data, via the
  \l{KeyDerivationParameters} option.  The input data to the key
  derivation function may either be specified within those parameters, or
  alternatively can be requested (by the secrets service) directly from
  the user if valid \l{InteractionParameters} are specified.  In that case,
  the user will be prompted to enter a passphrase, PIN, or other data which
  will then be used as the input key data by the key derivation function.

  See the following for an example of how to generate (via PBKDF2 derivation
  with HMAC-SHA512, from a user-supplied input passphrase) a 256-bit key
  intended for use with AES encryption and decryption operations, and store
  it in secure storage (assuming the prior existence of the collection named
  "ExampleCollection" stored by a specific crypto storage plugin):

  \code
  // Define the key metadata via a template.
  Sailfish::Crypto::Key keyTemplate;
  keyTemplate.setAlgorithm(Sailfish::Crypto::CryptoManager::AlgorithmAes);
  keyTemplate.setOrigin(Sailfish::Crypto::Key::OriginDevice);
  keyTemplate.setOperations(Sailfish::Crypto::CryptoManager::OperationEncrypt | Sailfish::Crypto::CryptoManager::OperationDecrypt);
  keyTemplate.setComponentConstraints(Sailfish::Crypto::Key::MetaData | Sailfish::Crypto::Key::PublicKeyData);

  // The key will be stored by the default crypto storage plugin
  // in the specified collection, with the given name.
  // NOTE: the collection must already exist, or this request will fail.
  // You can create new collections using the Sailfish OS Secrets API.
  keyTemplate.setIdentifier(
          Sailfish::Crypto::Key::Identifier(
              QLatin1String("ExampleKey"),
              QLatin1String("ExampleCollection"),
              Sailfish::Crypto::CryptoManager::DefaultCryptoStoragePluginName));

  // Specify the key derivation parameters which define how the key data
  // should be generated by the crypto plugin from the input data.
  Sailfish::Crypto::KeyDerivationParameters skdf;
  skdf.setKeyDerivationFunction(Sailfish::Crypto::CryptoManager::KdfPkcs5Pbkdf2);
  skdf.setKeyDerivationMac(Sailfish::Crypto::CryptoManager::MacHmac);
  skdf.setKeyDerivationDigestFunction(Sailfish::Crypto::CryptoManager::DigestSha512);
  skdf.setIterations(16384);
  skdf.setSalt(saltData); // 16 bytes, randomly generated, can be stored in plaintext in app data.
  skdf.setOutputKeySize(256);

  // Specify the interaction parameters which define how the the prompt
  // for the user to enter the input data should look and feel.
  Sailfish::Crypto::InteractionParameters uiParams;
  uiParams.setInputType(Sailfish::Crypto::InteractionParameters::AlphaNumericInput);
  uiParams.setEchoMode(Sailfish::Crypto::InteractionParameters::NormalEcho);

  // Ask the crypto service to perform the required operations.
  Sailfish::Crypto::GenerateStoredKeyRequest gskr;
  gskr.setManager(cryptoManager);
  gskr.setCryptoPluginName(Sailfish::Crypto::CryptoManager::DefaultCryptoStoragePluginName);
  gskr.setKeyTemplate(keyTemplate);
  gskr.setKeyDerivationParameters(skdf);
  gskr.setInteractionParameters(uiParams);
  gskr.startRequest();
  \endcode

  You can also generate an asymmetric cipher key; however, these are not
  derived from input key data, but instead generated randomly given certain
  starting parameters.  An example of generating an asymmetric RSA key pair
  follows:

  \code
  // Define the key metadata via a template.
  Sailfish::Crypto::Key keyTemplate;
  keyTemplate.setAlgorithm(Sailfish::Crypto::CryptoManager::AlgorithmRsa);
  keyTemplate.setOrigin(Sailfish::Crypto::Key::OriginDevice);
  keyTemplate.setOperations(Sailfish::Crypto::CryptoManager::OperationEncrypt
                           |Sailfish::Crypto::CryptoManager::OperationDecrypt
                           |Sailfish::Crypto::CryptoManager::OperationSign
                           |Sailfish::Crypto::CryptoManager::OperationVerify);
  keyTemplate.setComponentConstraints(Sailfish::Crypto::Key::MetaData | Sailfish::Crypto::Key::PublicKeyData);
  keyTemplate.setIdentifier(
          Sailfish::Crypto::Key::Identifier(
              QLatin1String("ExampleRsaKey"),
              QLatin1String("ExampleCollection"),
              Sailfish::Crypto::CryptoManager::DefaultCryptoStoragePluginName)));

  // Define some parameters for key-pair generation
  Sailfish::Crypto::RsaKeyPairGenerationParameters kpg;
  kpg.setModulusLength(4096);
  kpg.setPublicExponent(65537);
  kpg.setNumberPrimes(2);

  // Ask the crypto service to perform the required operations.
  Sailfish::Crypto::GenerateStoredKeyRequest gskr;
  gskr.setManager(cryptoManager);
  gskr.setCryptoPluginName(Sailfish::Crypto::CryptoManager::DefaultCryptoStoragePluginName);
  gskr.setKeyTemplate(keyTemplate);
  gskr.setKeyPairGenerationParameters(kpg);
  gskr.startRequest();
  \endcode
 */

/*!
  \brief Constructs a new GenerateStoredKeyRequest object with the given \a parent.
 */
GenerateStoredKeyRequest::GenerateStoredKeyRequest(QObject *parent)
    : Request(parent)
    , d_ptr(new GenerateStoredKeyRequestPrivate)
{
}

/*!
  \brief Destroys the GenerateStoredKeyRequest
 */
GenerateStoredKeyRequest::~GenerateStoredKeyRequest()
{
}

/*!
  \qmlproperty string GenerateStoredKeyRequest::cryptoPluginName
  \brief The name of the crypto plugin which the client wishes to perform the key generation operation
*/

/*!
  \brief Returns the name of the crypto plugin which the client wishes to perform the key generation operation
 */
QString GenerateStoredKeyRequest::cryptoPluginName() const
{
    Q_D(const GenerateStoredKeyRequest);
    return d->m_cryptoPluginName;
}

/*!
  \brief Sets the name of the crypto plugin which the client wishes to perform the key generation operation to \a pluginName
 */
void GenerateStoredKeyRequest::setCryptoPluginName(const QString &pluginName)
{
    Q_D(GenerateStoredKeyRequest);
    if (d->m_status != Request::Active && d->m_cryptoPluginName != pluginName) {
        d->m_cryptoPluginName = pluginName;
        if (d->m_status == Request::Finished) {
            d->m_status = Request::Inactive;
            emit statusChanged();
        }
        emit cryptoPluginNameChanged();
    }
}

/*!
  \qmlproperty InteractionParameters GenerateStoredKeyRequest::interactionParameters
  \brief The user input parameters which should be used when requesting the input data from the user
*/

/*!
  \brief Returns the user input parameters which should be used when requesting the input data from the user

  These interaction parameters are only meaningful if the template key
  algorithm is a symmetric cipher algorithm, and if a set of valid
  symmetric key derivation parameters are also specified for the request.

  If specified, the user will be prompted to enter some data
  (for example, a passphrase or PIN) which will then be used as input data
  to the key derivation function, which will produce the key data.
 */
Sailfish::Crypto::InteractionParameters
GenerateStoredKeyRequest::interactionParameters() const
{
    Q_D(const GenerateStoredKeyRequest);
    return d->m_uiParams;
}

/*!
  \brief Sets the user input parameters which should be used when requesting the input data from the user to \a uiParams
 */
void GenerateStoredKeyRequest::setInteractionParameters(
        const Sailfish::Crypto::InteractionParameters &uiParams)
{
    Q_D(GenerateStoredKeyRequest);
    if (d->m_status != Request::Active && d->m_uiParams != uiParams) {
        d->m_uiParams = uiParams;
        if (d->m_status == Request::Finished) {
            d->m_status = Request::Inactive;
            emit statusChanged();
        }
        emit interactionParametersChanged();
    }
}

/*!
  \qmlproperty KeyDerivationParameters GenerateStoredKeyRequest::keyDerivationParameters
  \brief The symmetric key derivation parameters which should be used to generate the secret key data
*/

/*!
  \brief Returns the symmetric key derivation parameters which should be used to generate the secret key data

  These interaction parameters are only meaningful if the template key
  algorithm is a symmetric cipher algorithm.
 */
Sailfish::Crypto::KeyDerivationParameters
GenerateStoredKeyRequest::keyDerivationParameters() const
{
    Q_D(const GenerateStoredKeyRequest);
    return d->m_skdfParams;
}

/*!
  \brief Sets the symmetric key derivation parameters which should be used to generate the secret key data to \a params
 */
void GenerateStoredKeyRequest::setKeyDerivationParameters(
        const Sailfish::Crypto::KeyDerivationParameters &params)
{
    Q_D(GenerateStoredKeyRequest);
    if (d->m_status != Request::Active && d->m_skdfParams != params) {
        d->m_skdfParams = params;
        if (d->m_status == Request::Finished) {
            d->m_status = Request::Inactive;
            emit statusChanged();
        }
        emit keyDerivationParametersChanged();
    }
}

/*!
  \qmlproperty KeyPairGenerationParameters GenerateStoredKeyRequest::keyPairGenerationParameters
  \brief The asymmetric key pair generation parameters which
         should be used to generate the public and private key data
*/

/*!
  \brief Returns the asymmetric key pair generation parameters which
         should be used to generate the public and private key data

  These parameters are only meaningful if the template key
  algorithm is an asymmetric cipher algorithm.
 */
KeyPairGenerationParameters
GenerateStoredKeyRequest::keyPairGenerationParameters() const
{
    Q_D(const GenerateStoredKeyRequest);
    return d->m_kpgParams;
}

/*!
  \brief Sets the asymmetric key pair generation parameters which
         should be used to generate the public and private key data to \a params
 */
void GenerateStoredKeyRequest::setKeyPairGenerationParameters(
        const KeyPairGenerationParameters &params)
{
    Q_D(GenerateStoredKeyRequest);
    if (d->m_status != Request::Active && d->m_kpgParams != params) {
        d->m_kpgParams = params;
        if (d->m_status == Request::Finished) {
            d->m_status = Request::Inactive;
            emit statusChanged();
        }
        emit keyPairGenerationParametersChanged();
    }
}

/*!
  \qmlproperty Key GenerateStoredKeyRequest::keyTemplate
  \brief The key which should be used as a template when generating the full key
*/

/*!
  \brief Returns the key which should be used as a template when generating the full key
 */
Key GenerateStoredKeyRequest::keyTemplate() const
{
    Q_D(const GenerateStoredKeyRequest);
    return d->m_keyTemplate;
}

/*!
  \brief Sets the key which should be used as a template when generating the full key to \a key
 */
void GenerateStoredKeyRequest::setKeyTemplate(const Key &key)
{
    Q_D(GenerateStoredKeyRequest);
    if (d->m_status != Request::Active && d->m_keyTemplate != key) {
        d->m_keyTemplate = key;
        if (d->m_status == Request::Finished) {
            d->m_status = Request::Inactive;
            emit statusChanged();
        }
        emit keyTemplateChanged();
    }
}

/*!
  \qmlproperty Key GenerateStoredKeyRequest::generatedKeyReference
  \brief Returns a key reference to the securely-stored generated key
  \note this value is only valid if the status of the request is \c Request.Finished.
*/

/*!
  \brief Returns a key reference to the securely-stored generated key

  Note: this value is only valid if the status of the request is Request::Finished.

  The key reference will contain metadata and a valid identifier, but no private or secret key data.
 */
Key GenerateStoredKeyRequest::generatedKeyReference() const
{
    Q_D(const GenerateStoredKeyRequest);
    return d->m_generatedKeyReference;
}

Request::Status GenerateStoredKeyRequest::status() const
{
    Q_D(const GenerateStoredKeyRequest);
    return d->m_status;
}

Result GenerateStoredKeyRequest::result() const
{
    Q_D(const GenerateStoredKeyRequest);
    return d->m_result;
}

QVariantMap GenerateStoredKeyRequest::customParameters() const
{
    Q_D(const GenerateStoredKeyRequest);
    return d->m_customParameters;
}

void GenerateStoredKeyRequest::setCustomParameters(const QVariantMap &params)
{
    Q_D(GenerateStoredKeyRequest);
    if (d->m_customParameters != params) {
        d->m_customParameters = params;
        if (d->m_status == Request::Finished) {
            d->m_status = Request::Inactive;
            emit statusChanged();
        }
        emit customParametersChanged();
    }
}

CryptoManager *GenerateStoredKeyRequest::manager() const
{
    Q_D(const GenerateStoredKeyRequest);
    return d->m_manager.data();
}

void GenerateStoredKeyRequest::setManager(CryptoManager *manager)
{
    Q_D(GenerateStoredKeyRequest);
    if (d->m_manager.data() != manager) {
        d->m_manager = manager;
        emit managerChanged();
    }
}

void GenerateStoredKeyRequest::startRequest()
{
    Q_D(GenerateStoredKeyRequest);
    if (d->m_status != Request::Active && !d->m_manager.isNull()) {
        d->m_status = Request::Active;
        emit statusChanged();
        if (d->m_result.code() != Result::Pending) {
            d->m_result = Result(Result::Pending);
            emit resultChanged();
        }

        QDBusPendingReply<Result, Key> reply =
                d->m_manager->d_ptr->generateStoredKey(d->m_keyTemplate,
                                                       d->m_kpgParams,
                                                       d->m_skdfParams,
                                                       d->m_uiParams,
                                                       d->m_customParameters,
                                                       d->m_cryptoPluginName);
        if (!reply.isValid() && !reply.error().message().isEmpty()) {
            d->m_status = Request::Finished;
            d->m_result = Result(Result::CryptoManagerNotInitializedError,
                                 reply.error().message());
            emit statusChanged();
            emit resultChanged();
        } else if (reply.isFinished()
                // work around a bug in QDBusAbstractInterface / QDBusConnection...
                && reply.argumentAt<0>().code() != Sailfish::Crypto::Result::Succeeded) {
            d->m_status = Request::Finished;
            d->m_result = reply.argumentAt<0>();
            d->m_generatedKeyReference = reply.argumentAt<1>();
            emit statusChanged();
            emit resultChanged();
            emit generatedKeyReferenceChanged();
        } else {
            d->m_watcher.reset(new QDBusPendingCallWatcher(reply));
            connect(d->m_watcher.data(), &QDBusPendingCallWatcher::finished,
                    [this] {
                QDBusPendingCallWatcher *watcher = this->d_ptr->m_watcher.take();
                QDBusPendingReply<Result, Key> reply = *watcher;
                this->d_ptr->m_status = Request::Finished;
                if (reply.isError()) {
                    this->d_ptr->m_result = Result(Result::DaemonError,
                                                   reply.error().message());
                } else {
                    this->d_ptr->m_result = reply.argumentAt<0>();
                    this->d_ptr->m_generatedKeyReference = reply.argumentAt<1>();
                }
                watcher->deleteLater();
                emit this->statusChanged();
                emit this->resultChanged();
                emit this->generatedKeyReferenceChanged();
            });
        }
    }
}

void GenerateStoredKeyRequest::waitForFinished()
{
    Q_D(GenerateStoredKeyRequest);
    if (d->m_status == Request::Active && !d->m_watcher.isNull()) {
        d->m_watcher->waitForFinished();
    }
}
